<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <link rel="stylesheet" href="../../public/css/bootstrap.min.css" />
        <link rel="stylesheet" href="../../public/css/bootstrap-icons.css" />
        <link rel="stylesheet" href="../../public/css/theme.css" />
        <link rel="stylesheet" href="../../public/css/action-menu.css" />
        <!-- <script src="../../components/set-theme.js"></script> -->
        <script src="../../public/js/vue/vue.global.prod.js"></script>
        <script src="../../components/icon-svg.js"></script>
        <script src="../../components/action-menu.js"></script>
        <script src="../../components/reconnect.js"></script>
        <script src="../../components/lepton.js"></script>
        <style>
            #app {
                height: 100vh;
                --tree-title: 35px;
                --tree-node-height: 22px;
                --tree-height: calc(100vh - var(--tree-title));
                width: 100%;
            }

            #lepton-svgs {
                display: none;
            }

            .title {
                height: var(--tree-title);
                padding: 0px 6px;
                display: flex;
                justify-content: space-between;
                align-items: center;
                user-select: none;
            }

            .title-text {
                cursor: pointer;
            }

            .actions-container {
                display: flex;
                height: 100%;
                align-items: center;
                margin: 0;
            }

            .action-item {
                margin-right: 4px;
                cursor: pointer;
                display: block;
                white-space: nowrap;
                border-radius: 5px;
            }

            .action-label {
                display: flex;
                align-items: center;
                justify-content: space-between;
                width: 22px;
                height: 22px;
                padding: 3px;
                text-align: center;
                user-select: none;
                text-decoration: none;
                position: relative;
            }

            .a {
                position: fixed;
                right: 14px;
                top: var(--tree-title);
                min-width: inherit;
            }

            .refresh {
                cursor: pointer;
            }

            #tree-window {
                height: var(--tree-height);
                overflow-x: hidden;
                width: 100%;
            }

            .tree {
                position: relative;
                left: 0;
                top: 0;
                width: 100%;
                transform: translate3d(0px, 0px, 0px);
                contain: strict;
            }

            .tree-node {
                position: absolute;
                left: 0;
                width: 100%;
                box-sizing: border-box;
                overflow: hidden;
                cursor: pointer;
                user-select: none;
            }

            .tree-row {
                display: flex;
                justify-content: flex-start;
                width: 100%;
                align-items: center;
            }

            .tree-node-indent {
            }

            .tree-node-collapsible {
                display: flex;
                align-items: center;
                padding-right: 6px;
            }

            .tree-node-content {
                flex: 1;
                display: flex;
                justify-content: flex-start;
                overflow: hidden;
            }

            .tree-node-title {
                display: flex;
                width: 100%;
                justify-content: flex-start;
            }

            .tree-node-icon {
                padding-right: 6px;
                display: flex;
                align-items: center;
            }

            .tree-node-label {
                text-overflow: ellipsis;
                overflow: hidden;
                white-space: nowrap;
            }

            .tree-node-actions {
                display: flex;
                justify-content: flex-start;
                flex-wrap: nowrap;
                align-items: center;
                padding-right: 8px;
            }

            .collapsed {
                transform: rotate(-90deg);
            }

            .tree-node-label-input {
                border: none;
                height: 20px;
            }

            :root {
                --drag-cancel-size: 40vw;
                --deag-word-r: 80%;
            }

            .drag-delete {
                position: fixed;
                bottom: 0;
                right: 0;
                z-index: 10;
            }

            .drag-delete-wapper {
                position: relative;
                padding: 1rem;
                border-radius: 5px 0px 0px 0px;
                display: flex;
                flex-direction: column;
                gap: 5px;
                justify-content: center;
                align-items: center;
                background-color: #e64240;
                color: white;
            }

            .deleteable {
                background-color: #cd3c39;
            }

            .dragging-target {
                border-bottom: 1px solid #cd3c39;
            }

            .dragging-target-inner {
                border: 1px solid #cd3c39;
            }
        </style>
    </head>
    <body class="theme">
        <svg id="lepton-svgs" class="d-none"></svg>
        <div id="app">
            <div class="title">
                <span class="title-text hover text-nowrap" @click="openController">{{name}}</span>
                <ul class="actions-container" role="toolbar" aria-label="资源管理器">
                    <li
                        class="action-item hover"
                        v-for="item in actions"
                        :key="label"
                        role="presentation"
                        :id="item.id"
                        @click="item.click"
                        custom-hover="true">
                        <a class="action-label" role="button" :aria-label="item.label" tabindex="0">
                            <icon-svg :name="item.icon" :size="item.iconSize"></icon-svg>
                        </a>
                    </li>
                </ul>
            </div>
            <div
                id="tree-window"
                class="scrollbar"
                @dragstart="dragstart"
                @dragover.prevent
                @dragend="dragend"
                @drop.prevent="drop"
                @dragenter="dragenter"
                @dragleave="dragleave">
                <div class="tree" :style="{height:nodeTotalHeight }">
                    <template v-for="(node,index) in tree" :key="node.path">
                        <div
                            class="tree-node hover"
                            tabindex="0"
                            draggable="true"
                            v-if="node.isView"
                            :style="createStyle(node,index)"
                            :class="{selected: activePath == node.path}"
                            :data-context-value="node.contextValue"
                            :data-path="node.path"
                            :data-index="index"
                            :data-is-file="node.isFile"
                            :data-parent-path="node.parentPath"
                            @click="onLoad(node,index)"
                            @dblclick="onOpen(node,index)"
                            @keyup.prevent="event=>onKeyup(event,node)">
                            <div class="tree-row">
                                <div
                                    class="tree-node-indent"
                                    :style="{minWidth: node.indent+'px'}"></div>
                                <div
                                    class="tree-node-collapsible"
                                    :style="{minWidth:'18px',minHeight:nodeHeight+'px'}">
                                    <icon-svg
                                        v-if="node.collapsibleState"
                                        :class="node.isExpanded || 'collapsed'"
                                        size="12px"
                                        name="arrows-down"></icon-svg>
                                </div>
                                <div class="tree-node-content">
                                    <div class="tree-node-title">
                                        <div
                                            class="tree-node-icon"
                                            :style="{minWidth:nodeHeight+'px',minHeight:nodeHeight+'px'}">
                                            <icon-svg :name="node.icon" size="18px"></icon-svg>
                                        </div>
                                        <input
                                            v-if="node.editable"
                                            :ref="node.path"
                                            class="tree-node-label-input"
                                            :class="{error: node.hasError}"
                                            v-model="node.newName"
                                            @input="onInput(node)"
                                            @blur="submitRename(node)"
                                            @keyup.enter="submitRename(node)" />
                                        <div v-else class="tree-node-label">{{node.label}}</div>
                                    </div>
                                    <div class="tree-node-actions">
                                        <icon-svg
                                            v-if="node.loading"
                                            name="loading"
                                            size="12px"></icon-svg>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </template>
                </div>
            </div>
            <action-menu
                @menu-click="menuClick"
                selector="#tree-window"
                :menus="menus"></action-menu>
            <div class="input-error error" :style="inputErrorStyle">
                <span class="input-message">{{ this.inputErrorStyle.msg}}</span>
            </div>
            <div
                class="drag-delete"
                v-if="drag.dragging"
                @dragover.prevent
                @drop.prevent="dropRemove"
                @dragenter="dragenterRemove"
                @dragleave="dragleaveRemove">
                <div class="drag-delete-wapper" :class="{deleteable:drag.deleteable}">
                    <icon-svg name="remove"></icon-svg>
                    <span class="drag-delete-text">删除</span>
                </div>
            </div>
            <div class="menu a" v-if="moreMenuState">
                <ul class="menu-action">
                    <li
                        class="menu-action-item hover"
                        v-for="item in dropdownMenus"
                        @click="item.click">
                        {{item.label}}
                    </li>
                </ul>
            </div>
            <Reconnect></Reconnect>
        </div>
        <script src="../../public/icons/server-icon.js"></script>
        <script src="../../public/icons/remove-icon.js"></script>
        <script src="../../public/icons/computer-icon.js"></script>
        <script src="../../public/icons/connection-icon.js"></script>
        <script src="../../public/icons/offline-icon.js"></script>
        <script src="../../public/icons/online-icon.js"></script>
        <script src="../../public/icons/disconnect-icon.js"></script>
        <script src="../../public/icons/arrows-down-icon.js"></script>
        <script src="../../public/icons/loading-icon.js"></script>
        <script src="../../public/icons/refresh-icon.js"></script>
        <script src="../../public/icons/shortcut-icon.js"></script>
        <script src="../../public/icons/create-file-icon.js"></script>
        <script src="../../public/icons/create-folder-icon.js"></script>
        <script src="../../public/icons/fold-icon.js"></script>
        <script src="../../public/icons/menu-icon.js"></script>
        <script src="../../public/icons/heartbeat-icon.js"></script>
        <script>
            const { createApp } = Vue
            const app = createApp({
                data() {
                    return {
                        tree: [],
                        scrollTop: 0,
                        maxHeight: 0,
                        nodeHeight: 22,
                        indent: 16,
                        svgContainer: {},
                        durationState: false,
                        name: '',
                        heart: null,
                        activePath: -1,
                        moreMenuState: false,
                        actions: [
                            {
                                label: '新建文件',
                                icon: 'create-file',
                                iconSize: '16px',
                                click: async () => {
                                    await this.onCommand('CREATE_FILE', {
                                        path: '/'
                                    })
                                    return this.refreshParentFolder({
                                        path: '/'
                                    })
                                }
                            },
                            {
                                label: '新建文件夹',
                                icon: 'create-folder',
                                iconSize: '16px',
                                click: async () => {
                                    await this.onCommand('CREATE_PATH', {
                                        path: '/'
                                    })
                                    return this.refreshParentFolder({
                                        path: '/'
                                    })
                                }
                            },
                            {
                                label: '刷新资源管理器',
                                icon: 'refresh',
                                iconSize: '16px',
                                click: () => {
                                    this.refresh({
                                        path: '/'
                                    })
                                }
                            },
                            {
                                label: '在资源管理器中折叠文件夹',
                                icon: 'fold',
                                iconSize: '16px',
                                click: () => {
                                    this.tree.forEach((it, index) => {
                                        if (it.isExpanded) {
                                            it.isExpanded = false
                                            const { start, count } = this.findChildren(
                                                it.path,
                                                index + 1
                                            )
                                            this.tree.splice(start, count)
                                            this.renderRange(this.scrollTop)
                                        }
                                    })
                                }
                            },
                            {
                                id: 'more-btn',
                                label: '更多菜单',
                                icon: 'menu',
                                iconSize: '16px',
                                click: () => {
                                    this.moreMenuState = !this.moreMenuState
                                }
                            }
                        ],
                        menus: [
                            {
                                label: '刷新',
                                when: value => ['folder'].includes(value),
                                command: 'extension.refresh',
                                onClick: node => this.refresh(node)
                            },
                            {
                                label: '新建文件',
                                when: value => ['folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('CREATE_FILE', node)
                                    return this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '新建目录',
                                when: value => ['folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('CREATE_PATH', node)
                                    return this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '打开',
                                when: value => ['file'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('OPEN', node)
                                }
                            },
                            {
                                label: '外部打开',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('OPEN_EXTERNAL', node)
                                }
                            },
                            {
                                label: '复制路径',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('COPY_PATH', node)
                                }
                            },
                            {
                                label: '复制',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('COPY', node)
                                }
                            },
                            {
                                label: '粘贴',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('PASTE', node)
                                }
                            },
                            {
                                label: '备份(时间戳)',
                                when: value => ['file', 'folder', 'docker-compose'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('BACKUP_TIMEPOINT', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '备份(版本号)',
                                when: value => ['file', 'folder', 'docker-compose'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('BACKUP_VERSION', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '压缩',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('GZIP', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '解压',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('UNGZIP', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '远程移动',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('MOVE', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '删除',
                                when: value => ['file', 'folder', 'docker-compose'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('REMOVE_TO_TRASHCAN', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '重命名',
                                when: value => ['file', 'folder', 'docker-compose'].includes(value),
                                onClick: async node => {
                                    this.onRename(node)
                                }
                            },
                            {
                                label: '下载到本地',
                                when: value => ['file', 'docker-compose'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('DOWNLOAD_FILE', node)
                                }
                            },
                            {
                                label: '下载到本地',
                                when: value => ['folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('DOWNLOAD_PATH', node)
                                }
                            },
                            {
                                label: '上传文件',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('UPLOAD_FILE', node)
                                    if (node.isFile) {
                                        return this.refreshParentFolder(node)
                                    }
                                    this.refresh(node)
                                }
                            },
                            {
                                label: '上传文件(压缩)',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('ZIP_UPLOAD_FILE', node)
                                    if (node.isFile) {
                                        return this.refreshParentFolder(node)
                                    }
                                    this.refresh(node)
                                }
                            },
                            {
                                label: '上传目录',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('UPLOAD_PATH', node)
                                    if (node.isFile) {
                                        return this.refreshParentFolder(node)
                                    }
                                    this.refresh(node)
                                }
                            },
                            {
                                label: '上传目录(压缩)',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('ZIP_UPLOAD_PATH', node)
                                    if (node.isFile) {
                                        return this.refreshParentFolder(node)
                                    }
                                    this.refresh(node)
                                }
                            },
                            {
                                label: '创建快捷方式',
                                when: value => ['file', 'folder'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('CREATE_SHORTCUT', node)
                                }
                            },
                            {
                                label: '同步到本地',
                                when: value => ['file'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('SERVER_FILE_SYNC_TO_CLIENT', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '同步到远程',
                                when: value => ['file'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('CLIENT_FILE_SYNC_TO_SERVER', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '还原',
                                when: value => ['trashcan-file'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('RESTORE_TRASHCAN_ITEM', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '删除',
                                when: value => ['trashcan-file'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('REMOVE_TRASHCAN_FILE', node)
                                    this.refreshParentFolder(node)
                                }
                            },
                            {
                                label: '清空回收站',
                                when: value => ['trashcan'].includes(value),
                                onClick: async node => {
                                    await this.onCommand('CLEAR_TRASHCAN', node)
                                    this.refresh(node)
                                }
                            }
                        ],
                        inputErrorStyle: {
                            display: 'none',
                            position: 'fixed',
                            left: 0,
                            top: 0
                        },
                        drag: {
                            overNode: null,
                            start: null,
                            end: null,
                            dragging: false,
                            deleteable: false,
                            timerId: null
                        },
                        dropdownMenus: [
                            {
                                label: '打开终端',
                                click: async () => {
                                    this.moreMenuState = false
                                    await this.onCommand('OPEN_TERMINAL')
                                }
                            },
                            {
                                label: '端口转发',
                                click: async () => {
                                    this.moreMenuState = false
                                    await this.onCommand('OPEN_PORT_FORWARD')
                                }
                            },
                            {
                                label: '生成密钥',
                                click: async () => {
                                    this.moreMenuState = false
                                    await this.onCommand('CREATE_KEYPAIR')
                                }
                            },
                            {
                                label: '设置登陆密钥',
                                click: async () => {
                                    this.moreMenuState = false
                                    await this.onCommand('SET_LOGIN_KEYPAIR')
                                }
                            },
                            {
                                label: 'Docker',
                                click: async () => {
                                    this.moreMenuState = false
                                    await this.onCommand('OPEN_DOCKER')
                                }
                            },
                            {
                                label: '创建桌面快捷方式',
                                click: async () => {
                                    this.moreMenuState = false
                                    await this.onCommand('SERVER_SHORTCUT')
                                }
                            },
                            {
                                label: '注册上传菜单',
                                click: async () => {
                                    this.moreMenuState = false
                                    await this.onCommand('SET_UPLOAD_MENU')
                                }
                            },
                            {
                                label: '删除上传菜单',
                                click: async () => {
                                    this.moreMenuState = false
                                    await this.onCommand('DEL_UPLOAD_MENU')
                                }
                            },
                            {
                                label: '关闭连接',
                                click: async () => {
                                    await this.onCommand('CLOSE_SERVER')
                                    this.moreMenuState = false
                                }
                            }
                        ]
                    }
                },
                mounted() {
                    this.initChannel()
                    this.treeContainer = document.getElementById('tree-window')
                    this.treeContainer.addEventListener('scroll', event => {
                        this.scrollTop = this.treeContainer.scrollTop
                        this.renderRange(this.scrollTop)
                    })
                    let timeout
                    window.addEventListener('resize', () => {
                        clearTimeout(timeout)
                        timeout = setTimeout(() => {
                            this.renderRange(this.scrollTop)
                        }, 300)
                    })
                    this.maxHeight = this.treeContainer.clientHeight
                    this.svgContainer = document.getElementById('lepton-svgs')
                    document.addEventListener('paste', event => {
                        this.onCommand('PASTE')
                    })
                    document.addEventListener('click', event => {
                        const more = document.getElementById('more-btn')
                        if (!more.contains(event.target)) {
                            this.moreMenuState = false
                        }
                    })
                    document.addEventListener('contextmenu', event => {
                        event.preventDefault()
                        const more = document.getElementById('more-btn')
                        if (!more.contains(event.target)) {
                            this.moreMenuState = false
                        }
                    })
                    window.addEventListener('blur', () => {
                        this.moreMenuState = false
                    })
                },
                computed: {
                    nodeTotalHeight() {
                        return this.tree.length * this.nodeHeight + 'px'
                    }
                },
                methods: {
                    setHeartbeat(state) {
                        this.heart = state
                    },
                    async onInit() {
                        this.onFn('onInit').then(data => {
                            this.name = data.name
                            this.host = data.host
                        })
                        this.onLoad()
                        this.onFnScript('context-menu', 'menus').then(menus => {
                            const useds = menus.map(it => ({
                                id: it.id,
                                label: it.label,
                                when: value => it.when.includes(value),
                                onClick: node =>
                                    this.onFnScript('context-menu', 'onClick', it.id, node)
                            }))
                            this.menus.push(...useds)
                        })
                    },
                    refreshParentFolder(node) {
                        this.loadNode(node.parentPath, true, node)
                    },
                    refresh(node) {
                        if (node.isExpanded) {
                            node.loading = true
                            return this.loadNode(node.path, true, node)
                        }
                        this.onFn('onRemoveCache', node.path)
                    },
                    createStyle(node, index) {
                        return {
                            height: this.nodeHeight + 'px',
                            lineHeight: this.nodeHeight + 'px',
                            top: index * this.nodeHeight + 'px'
                        }
                    },
                    async loadNode(path, noUsedCache, node) {
                        try {
                            const data = await this.onFn('onLoad', {
                                path,
                                noUsedCache
                            })
                            this.onLoadResponse(data)
                        } finally {
                            if (node) {
                                node.loading = false
                            }
                        }
                    },
                    mergeToTree(nodes, parentPath, parentIndex) {
                        console.time('合并节点耗时')
                        let children = new Map()
                        let total = 0
                        for (let i = parentIndex; i < this.tree.length; i++) {
                            let node = this.tree[i]
                            if (node.parentPath == parentPath) {
                                const values = [node]
                                for (let j = i + 1; j < this.tree.length; j++) {
                                    let child = this.tree[j]
                                    if (child.path.startsWith(node.path + '/')) {
                                        values.push(child)
                                        i = j
                                        total++
                                    } else {
                                        break
                                    }
                                }
                                children.set(node.path, {
                                    isVisit: false,
                                    values
                                })
                                total++
                            } else {
                                break
                            }
                        }
                        let values
                        for (const node of nodes) {
                            let current = children.get(node.path)
                            if (current) {
                                values = current.values
                                current.isVisit = true
                            } else {
                                if (values) {
                                    values.push(node)
                                } else {
                                    values = [node]
                                    children.set(node.path, {
                                        isVisit: true,
                                        values
                                    })
                                }
                            }
                        }
                        const changed = []
                        for (const [key, value] of children) {
                            if (value.isVisit) {
                                changed.push(...value.values)
                            }
                        }
                        this.tree.splice(parentIndex, total, ...changed)
                        console.timeEnd('合并节点耗时')
                    },
                    findChildren(path, index) {
                        const data = {
                            start: index,
                            count: 0
                        }
                        for (let i = index; i < this.tree.length; i++) {
                            const node = this.tree[i]
                            if (node.path.startsWith(path + '/')) {
                                data.count++
                            } else {
                                break
                            }
                        }
                        console.log('插叙还能', path, data)
                        return data
                    },
                    onLoad(node, index) {
                        this.activePath = node?.path
                        if (node) {
                            if (node.collapsibleState) {
                                node.isExpanded = !node.isExpanded
                                if (node.isExpanded) {
                                    node.loading = true
                                    this.loadNode(node.path, false, node)
                                } else {
                                    let start = index + 1
                                    let deleteCount = 0
                                    for (let i = start; i < this.tree.length; i++) {
                                        const item = this.tree[i]
                                        if (!item.path.startsWith(node.path + '/')) {
                                            break
                                        }
                                        deleteCount++
                                    }
                                    this.tree.splice(start, deleteCount)
                                    this.renderRange(this.scrollTop)
                                }
                            }
                        } else {
                            this.loadNode('/')
                        }
                    },
                    onLoadResponse(data) {
                        const { path, nodes } = data
                        if (path == '/') {
                            nodes.forEach((node, index) => {
                                node.indent = 6
                            })
                            this.mergeToTree(nodes, '/', 0)
                        } else {
                            let index = this.tree.findIndex(it => path == it.path)
                            if (index != -1) {
                                const parent = this.tree[index]
                                parent.loading = false
                                if (parent.isExpanded) {
                                    nodes.forEach((node, index) => {
                                        node.indent = parent.indent + this.indent
                                    })
                                    this.mergeToTree(nodes, path, index + 1)
                                }
                            }
                        }
                        this.renderRange(this.scrollTop)
                    },
                    onChangeNode(changed) {
                        const node = this.tree.find(it => it.path == changed.path)
                        Object.assign(node, changed)
                    },
                    renderRange(scrollTop) {
                        const stop = scrollTop
                        const etop = scrollTop + this.maxHeight
                        const sidx = Math.ceil(stop / this.nodeHeight)
                        const eidx = Math.ceil(etop / this.nodeHeight)
                        this.tree.forEach((node, index) => {
                            node.isView = index >= sidx && index <= eidx
                        })
                    },
                    menuClick(menu, dataset) {
                        const node = this.tree.find(it => it.path == dataset.path)
                        if (menu.onClick) {
                            menu.onClick(node)
                        }
                    },
                    async onCommand(command, data) {
                        return await this.onFn('onCommand', {
                            command,
                            data
                        })
                    },
                    onInsertNode({ node, svg }) {
                        const position = this.tree.findIndex(it => node.parentPath == it.path)
                        if (position != -1) {
                            const parent = this.tree[position]
                            node.indent = parent.indent + this.indent
                            this.tree.splice(position + 1, 0, [node])
                        } else {
                            node.indent = 6
                            this.tree.unshift(node)
                        }
                    },
                    onRename(node) {
                        node.editable = true
                        node.newName = node.label
                        this.$nextTick(() => {
                            const input = this.$refs[node.path].at(0)
                            input?.focus()
                            let start = 0
                            let end = node.newName.length
                            if (node.isFile) {
                                end = node.newName.indexOf('.')
                            }
                            input?.setSelectionRange(start, end)
                        })
                    },
                    onInput(node) {
                        if (!node.newName?.trim()) {
                            node.hasError = true
                            const input = this.$refs[node.path].at(0)
                            const rect = input.getBoundingClientRect()
                            const x = rect.left + window.scrollX
                            const y = rect.bottom + window.scrollY
                            this.inputErrorStyle.left = x + 'px'
                            this.inputErrorStyle.top = y + 'px'
                            this.inputErrorStyle.width = rect.width + 'px'
                            this.inputErrorStyle.display = 'flex'
                            this.inputErrorStyle.msg = '必须提供文件或者文件夹名'
                        } else {
                            this.inputErrorStyle.display = 'none'
                            node.hasError = false
                        }
                    },
                    async submitRename(node) {
                        try {
                            await this.onCommand('RENAME', node)
                            node.label = node.newName
                            node.newName = ''
                            node.editable = false
                        } catch (error) {
                            const input = this.$refs[node.path].at(0)
                            const rect = input.getBoundingClientRect()
                            const x = rect.left + window.scrollX
                            const y = rect.bottom + window.scrollY
                            this.inputErrorStyle.left = x + 'px'
                            this.inputErrorStyle.top = y + 'px'
                            this.inputErrorStyle.width = rect.width + 'px'
                            this.inputErrorStyle.msg = error.message
                            this.inputErrorStyle.display = 'flex'
                            node.hasError = true
                        }
                    },
                    onOpen(node) {
                        if (node.isFile) {
                            this.activePath = node.path
                            this.onCommand('OPEN', node)
                        }
                    },
                    async onKeyup(event, node) {
                        switch (event.key) {
                            case 'F2':
                                this.onRename(node)
                                break
                            case 'Delete':
                                await this.onCommand('REMOVE_TO_TRASHCAN', node)
                                this.refreshParentFolder(node)
                                break
                        }
                    },
                    openController() {
                        this.onCommand('OPEN_CONTROLLER')
                    },
                    // 拖动元素
                    dragstart(event) {
                        this.drag.start = event.target.dataset
                        this.drag.dragging = true
                    },
                    dragend() {
                        this.drag.dragging = false
                        clearTimeout(this.drag.timerId)
                    },
                    dragenter(event) {
                        let temp = event.target
                        let overNode
                        while (temp && temp != this.target) {
                            const value = temp.draggable
                            if (value) {
                                overNode = temp
                                break
                            }
                            temp = temp.parentElement
                        }
                        clearTimeout(this.drag.timerId)
                        this.drag.overNode = overNode
                        if (overNode) {
                            const dataset = this.drag.overNode.dataset
                            const index = Number(dataset.index)
                            if (dataset.isFile == 'false') {
                                this.drag.timerId = setTimeout(() => {
                                    const node = this.tree[index]
                                    if (node.isExpanded) {
                                        return
                                    }
                                    node.isExpanded = true
                                    node.loading = true
                                    this.loadNode(node.path, false, node)
                                }, 1500)
                            }
                            overNode.classList.add('selected')
                        }
                    },
                    dragleave(event) {
                        let temp = event.target
                        let overNode
                        while (temp && temp != this.target) {
                            const value = temp.draggable
                            if (value) {
                                overNode = temp
                                break
                            }
                            temp = temp.parentElement
                        }
                        overNode?.classList.remove('selected')
                    },
                    async drop(event) {
                        if (this.drag.overNode) {
                            const end = this.drag.overNode.dataset
                            const sourceIndex = parseInt(this.drag.start.index)
                            const targetIndex = parseInt(end.index)
                            const sourcePath = this.drag.start.path
                            let targetPath
                            if (end.isFile == 'true') {
                                targetPath = end.parentPath
                            } else {
                                targetPath = end.path
                            }
                            const isOk = await this.onCommand('MOVE_DRAG', {
                                host: this.host,
                                sourcePath,
                                targetPath,
                                isFile: this.drag.start.isFile == 'true'
                            })
                            if (isOk) {
                                const node = this.tree.at[targetIndex]
                                this.refresh(node)
                            }
                        }
                    },
                    // 拖动删除
                    dragenterRemove() {
                        this.drag.deleteable = true
                    },
                    dragleaveRemove() {
                        this.drag.deleteable = false
                    },
                    async dropRemove() {
                        const sourceIndex = parseInt(this.drag.start.index)
                        const node = this.tree[sourceIndex]
                        await this.onCommand('REMOVE_NO_CONFIRM', node)
                        this.refreshParentFolder(node)
                    }
                }
            })
            app.use(window.lepton)
            app.mount('#app')
        </script>
    </body>
</html>
